package jmcnet.libcommun.socketpool
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.SecurityErrorEvent;
	import flash.events.TimerEvent;
	import flash.net.Socket;
	import flash.net.sendToURL;
	import flash.utils.Timer;
	
	import jmcnet.libcommun.logger.JMCNetLog4JLogger;
	
	import mx.collections.ArrayCollection;
	import mx.utils.ObjectUtil;

	/**
	 * The socket pool class permits to have a pool of generic socket. The principle is the following :
	 * - initialize it (with CTOR arguments or use default parameters) -> this sends a EVENT_POOL_INITIALIZED event,
	 * - connect all socket in the pool by calling connect -> when the 1rst connection is done, it sends a EVENT_POOL_CONNECTED event,
	 * - get a free socket by calling getFreeSocket. If one is available, it is returns. If not caller has to wait for an EVENT_FREE_SOCKET message. The socket can be retrieved by calling getFreeSocket,
	 * - release a used socket by calling releaseSocket
	 * - disconnect all socket of the pool by calling disconnect -> this sends a EVENT_POOL_DISCONNECTED event
	 */
	[Event(name=EVENT_POOL_INITIALIZED, type="jmcnet.libcommun.socketpool.EventPoolSocket")]
	[Event(name=EVENT_POOL_CONNECTED, type="jmcnet.libcommun.socketpool.EventPoolSocket")]    // dispatched when first socket is added in freeSocketList
	[Event(name=EVENT_POOL_DISCONNECTED, type="jmcnet.libcommun.socketpool.EventPoolSocket")] 
	[Event(name=EVENT_FREE_SOCKET, type="jmcnet.libcommun.socketpool.EventPoolSocket")]       // dispatched when a new socket is added in freeSocketList
	public class SocketPool extends EventDispatcher
	{
		private static var log:JMCNetLog4JLogger = JMCNetLog4JLogger.getLogger(SocketPool);
		
		public static var logSocketPool:Boolean=false;
		
		public static const EVENT_POOL_INITIALIZED:String="poolInitialized";
		public static const EVENT_POOL_CONNECTED:String="poolConnected";
		public static const EVENT_POOL_DISCONNECTED:String="poolDisconnected";
		public static const EVENT_FREE_SOCKET:String="freeSocket";
		
		// Min socket in pool at init
		private var _min:int=0;
		// Max socket in pool
		private var _max:int=0;
		// Timeout before a used socket expire. If occurs, the socket is closed and not recycled
		private var _timeoutMs:uint=0; // 0 means never expires
		// Timer to check pool healness
		private var _timer:Timer=null;
		
		private var _hostname:String=null;
		private var _port:int=0;
		
		// The list of sockets
		private var _inCreationSocket:ArrayCollection = new ArrayCollection();
		private var _freeSocket:ArrayCollection = new ArrayCollection();
		private var _inUseSocket:ArrayCollection = new ArrayCollection();
		
		// The callback when a connection is established
		private var _onConnectCallback:Function = null;
		
		private const HEALTH_CHECK_TEMPO_MS:uint = 5000;
		private const CLOSE_UNUSED_SOCKET_TEMPO_MS:uint = 60000;
		
		public function SocketPool(min:int=0, max:int=100, timeoutMs:uint=10000) {
			_min = min; _max = max; _timeoutMs = timeoutMs;
			_timer = new Timer(HEALTH_CHECK_TEMPO_MS, 0);
			_timer.addEventListener(TimerEvent.TIMER, onTimerEvent);
			
		}
		
		public function connect(hostname:String, port:int, onConnectCallback:Function=null):void {
			if (logSocketPool) log.info("SocketPool connecting to '"+hostname+":"+port);
			_onConnectCallback = onConnectCallback;
			initPool();

			_hostname = hostname;
			_port = port;
			for each (var socket:TimedSocket in _inCreationSocket) {
				if (logSocketPool) log.debug("Calling socket.connect hostname="+hostname+" port="+port);
				socket.connect(hostname, port);
			}
			
			_timer.start();
		}
		
		public function disconnect():void {
			if (logSocketPool) log.info("SocketPool disconnecting from '"+_hostname+":"+_port);
			_hostname = null;
			_port = 0;
			for each (var socket:TimedSocket in _freeSocket) {
				socket.close();
			}
			
			for each (socket in _inUseSocket) {
				socket.close();
			}
			
			_freeSocket.removeAll();
			_inUseSocket.removeAll();
			_timer.stop();
			
			// Send a disconnect event
			this.dispatchEvent(new EventSocketPool(EVENT_POOL_DISCONNECTED));
		}
		
		public function connected():Boolean {
			return _freeSocket.length + _inUseSocket.length > 0; 
		}
		
		/**
		 * Returns a free connected socket. If there is not more connection available, returns null and mark that one demand is not satisfied
		 */
		public function getFreeSocket():TimedSocket {
			if (logSocketPool) log.debug("getFreeSocket : requesting for a free socket.");
			var socket:TimedSocket;
			var fl:int = _freeSocket.length;
			var iul:int = _inUseSocket.length;
			var icl:int = _inCreationSocket.length;
			
			if (fl <= 0) {
				if (logSocketPool) log.warn("getFreeSocket : there is no more free socket. freeSockets : "+fl+" inUseSockets : "+iul+" max sockets : "+_max);
				return null;
			}
			socket = _freeSocket.removeItemAt(0) as TimedSocket;
			fl--;
			// Add this socket into inUse list
			_inUseSocket.addItem(socket);
			iul++;
			
			socket.inUseTime = new Date().getTime();
			socket.releaseTime = 0;
			if (logSocketPool) log.info("getFreeSocket : we have found a free socket #"+socket.id+". FreeSockets : "+fl+" inUseSockets : "+iul+" max sockets : "+_max);
			
			// if freeSocket is near empty, try to create new connected socket for next time
			if (fl <= _min/5 && fl + iul + icl < _max ) {
				if (logSocketPool) log.info("Number of free socket is low. Instanciate new one");
				// instanciate more sockets
				for (var i:uint=0; i < Math.max(1, (_max - fl - iul - icl)/2); i++) {
					createNewSocket();
				}
			}
			
			return socket;
		}
		
		public function releaseSocket(socket:TimedSocket):void {
			if (logSocketPool) log.debug("SocketPool : releasing an 'in use' socket #"+socket.id);
			var idx:int;
			if ((idx = _inUseSocket.getItemIndex(socket)) != -1) {
				_inUseSocket.removeItemAt(idx);
				// Put it into freeSocket list
				_freeSocket.addItem(socket);
			}
			else log.warn("SocketPool : releasing an unknown socket #"+socket.id+" socket="+ObjectUtil.toString(socket));
			
			socket.inUseTime = 0;
			socket.releaseTime = new Date().getTime();
			
			var fl:int = _freeSocket.length;
			var iul:int = _inUseSocket.length;
			if (logSocketPool) log.info("SocketPool : we have release socket #"+socket.id+". FreeSockets : "+fl+" inUseSockets : "+iul+" max sockets : "+_max);
			
			// send a message indicating that there is a Free available socket
			if (fl > 0) {
				if (logSocketPool) log.evt("Dispatching Free socket event");
				dispatchEvent(new EventSocketPool(EVENT_FREE_SOCKET));
			}
		}
		
		public function getPoolInfos():PoolInfos {
			var pi:PoolInfos = new PoolInfos();
			pi.nbFreeSockets = _freeSocket.length;
			pi.nbInCreationSockets = _inCreationSocket.length;
			pi.nbInUseSockets = _inUseSocket.length;
			pi.nbTotalSockets = pi.nbFreeSockets+pi.nbInCreationSockets+pi.nbInUseSockets;
			pi.minSockets = _min;
			pi.maxSockets = _max;
			
			return pi;
		}
		
		private function initPool():void {
			if (logSocketPool) log.info("SocketPool : initialization of pool. Min="+_min+" max="+_max+" timeout="+_timeoutMs);
			for (var i:int=_inCreationSocket.length; i<_min;i++) {
				createNewSocket();
			}
			if (logSocketPool) log.evt("Dispatching Pool initialized event");
			this.dispatchEvent(new EventSocketPool(EVENT_POOL_INITIALIZED));
		}
		
		protected function onTimerEvent(event:TimerEvent):void {
			var poolInfos:PoolInfos = getPoolInfos();
			if (logSocketPool) log.debug("SocketPool:onTimerEvent : checking pool's health nbSocketInCreation="+poolInfos.nbInCreationSockets+" nbSocketFree="+poolInfos.nbFreeSockets+" nbSocketInUse="+poolInfos.nbInUseSockets);
			var socket:TimedSocket;
			var time:Number = new Date().getTime();
			var nbSocketToCreate:uint=0;
			
			var toClose:ArrayCollection = new ArrayCollection();
			
			// timeout inUse socket
			if (poolInfos.nbInUseSockets > 0) {
				for each (socket in _inUseSocket) {
					if (time - socket.inUseTime > _timeoutMs) {
						log.warn("SocketPool : timeout. The socket #"+socket.id+" is in use for "+(time - socket.inUseTime)+" milliseconds.");
						toClose.addItem(socket);
						poolInfos.nbInUseSockets--;
						nbSocketToCreate++;
					}
				}
				// close socket
			}
			
			// if > min open socket for long time, close some free
			if (poolInfos.nbFreeSockets > _min && poolInfos.nbFreeSockets + poolInfos.nbInUseSockets + nbSocketToCreate > _min) {
				for each (socket in _freeSocket) {
					if (time - socket.releaseTime > CLOSE_UNUSED_SOCKET_TEMPO_MS) {
						if (logSocketPool) log.info("SocketPool : socket unused for long time. The socket #"+socket.id+" is not used for "+(time - socket.releaseTime)+" milliseconds.");
						toClose.addItem(socket);
						poolInfos.nbFreeSockets --;
						// we keep min open socket
						if (poolInfos.nbFreeSockets + poolInfos.nbInUseSockets + nbSocketToCreate <= _min) break;
					}
				}
			}
			
			// Close sockets to be closed
			for each (socket in toClose) {
				if (logSocketPool) log.debug("SocketPool : closing socket #"+socket.id);
				socket.close();
				removeSocketFromPool(socket);
			}
			
			// We open nbSocketToOpen socket
			while (nbSocketToCreate > 0) {
				createNewSocket();
				nbSocketToCreate--;
			}
		}
		
		private function createNewSocket():TimedSocket {
			var socket:TimedSocket = new TimedSocket();
			socket.addEventListener(Event.CLOSE,onSocketClose);
			socket.addEventListener(Event.CONNECT, onSocketOpen);
			socket.addEventListener(IOErrorEvent.IO_ERROR,onSocketIoError);
			socket.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onSocketSecurityError);
			_inCreationSocket.addItem(socket);
			if (_hostname != null && _port != 0) socket.connect(_hostname, _port);
			if (logSocketPool) log.debug("SocketPool::createNewSocket. New socket #"+socket.id+" created");
			return socket;
		}
		
		protected function onSocketOpen(event:Event):void {
			// The socket is now open, remove it from _inCreation and put it into _free
			var socket:TimedSocket = event.target as TimedSocket;
			
			if (logSocketPool) log.evt("The socket #"+socket.id+" is connected with MongoDB.");
			
			// If there is a callback, call it (after putting socket in freeSocket)
			if (_onConnectCallback != null) {
				if (logSocketPool) log.debug("Call to onConnectCallback");
				_onConnectCallback(socket);
			}
			
			addSocketInFreeList(socket);
		}
		
		protected function addSocketInFreeList(socket:TimedSocket):void {
			if (logSocketPool) log.debug("Calling addSocketInFreeList socket #"+socket.id);
			
			var idx:int;
			if ((idx = _inCreationSocket.getItemIndex(socket)) != -1) _inCreationSocket.removeItemAt(idx);
			else log.warn("Trying to remove a connected socket from inCreation list, but socket #"+socket.id+"is unknown");
			_freeSocket.addItem(socket);
			if (_freeSocket.length == 1 && _inUseSocket.length <= 0) {
				// if it's the first connected socket send an event
				if (logSocketPool) log.evt("Dispatching Pool connected event (it's the first socket)");
				this.dispatchEvent(new EventSocketPool(EVENT_POOL_CONNECTED));
			}
			else {
				if (logSocketPool) log.evt("Dispatching Free socket #"+socket.id+" event");
				// send a message indicating that there is a Free available socket
				dispatchEvent(new EventSocketPool(EVENT_FREE_SOCKET));	
			}
			if (logSocketPool) log.debug("EndOf addSocketInFreeList socket #"+socket.id);
		}
		
		private function onSocketClose(event:Event):void {
			var socket:TimedSocket = event.target as TimedSocket;
			if (logSocketPool) log.evt("Connection with MongoDB database has closed. Socket #"+socket.id);
			// the socket is closed, remove it from pool
			removeSocketFromPool(socket);
		}
		
		private function onSocketIoError(event:IOErrorEvent):void {
			var socket:TimedSocket = event.target as TimedSocket;
			log.error("IOError while trying to connect to server : "+event.text+". Socket #"+socket.id);
			// the socket is in eror, remove it from pool
			removeSocketFromPool(socket);
			 
		}
		
		private function onSocketSecurityError(event:SecurityErrorEvent):void {
			var socket:TimedSocket = event.target as TimedSocket;
			log.error("SecurityError while trying to connect with server : "+event.text+". Socket #"+socket.id);
			// the socket is in error, remove it from pool
			removeSocketFromPool(socket);
		}
		
		protected function removeSocketFromPool(socket:TimedSocket):void {
			if (logSocketPool) log.debug("Calling removeSocketFromPool socket #"+socket.id);
			var idx:int;
			if ((idx = _inCreationSocket.getItemIndex(socket)) != -1) _inCreationSocket.removeItemAt(idx);
			else if ((idx = _freeSocket.getItemIndex(socket)) != -1) _freeSocket.removeItemAt(idx);
			else if ((idx = _inUseSocket.getItemIndex(socket)) != -1) _inUseSocket.removeItemAt(idx);
			else log.warn("SocketPool : Trying to remove an unknown socket. socket #"+socket.id);
			
			// if there is no more active socket, send pool closed
			if (_freeSocket.length + _inUseSocket.length <= 0) {
				if (logSocketPool) log.evt("Dispatching Pool Disconnected event");
				this.dispatchEvent(new EventSocketPool(EVENT_POOL_DISCONNECTED));
			}
		}
	}
}